package com.jingdianjichi.subject.infra.config;

import com.jingdianjichi.subject.common.util.LoginUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.*;

/**
 * MyBatis拦截器，用于自动填充实体的公共字段，如createBy、createTime等。
 * 它通过拦截Executor的update方法实现，在执行SQL前对参数进行处理。
 */
@Component
@Slf4j
@Intercepts({@Signature(type = Executor.class, method = "update", args = {
        MappedStatement.class, Object.class
})})
public class MybatisInterceptor implements Interceptor {

    /**
     * 拦截方法，当MyBatis执行update操作时被调用。
     * 根据SQL命令类型（插入、更新），自动填充创建者、创建时间、更新者、更新时间等字段。
     *
     * @param invocation 拦截的方法调用
     * @return 方法调用的返回值
     * @throws Throwable 可能抛出的异常
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取MyBatis传递过来的映射声明对象，它包含了SQL语句的类型（如SELECT、INSERT、UPDATE）。
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        // 从映射声明中获取SQL命令的类型。
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        // 获取执行SQL语句时传入的参数对象。
        Object parameter = invocation.getArgs()[1];
        // 如果参数对象为null，则直接执行原方法并返回结果，不进行任何处理。
        if (parameter == null) {
            return invocation.proceed();
        }
        // 尝试获取当前登录用户的ID，此ID将用于填充创建者和更新者字段。
        String loginId = LoginUtil.getLoginId();
        // 如果无法获取登录用户ID（如用户未登录），则直接执行原方法并返回结果，不进行字段填充。
        if (StringUtils.isBlank(loginId)) {
            return invocation.proceed();
        }
        // 对于INSERT和UPDATE类型的SQL命令，调用replaceEntityProperty方法自动填充相关字段。
        if (SqlCommandType.INSERT == sqlCommandType || SqlCommandType.UPDATE == sqlCommandType) {
            replaceEntityProperty(parameter, loginId, sqlCommandType);
        }
        // 执行原方法并返回结果。
        return invocation.proceed();
    }

    /**
     * 根据操作的参数类型，决定是直接处理单个实体对象还是遍历Map中的每个实体对象进行处理。
     * 这里处理的是为实体对象的创建者、创建时间、更新者、更新时间等字段自动赋值。
     *
     * @param parameter 参数对象，可能是单个实体对象或者包含多个实体对象的Map。
     * @param loginId 当前操作用户的ID，用于填充创建者和更新者字段。
     * @param sqlCommandType SQL操作的类型（INSERT或UPDATE），根据类型决定填充哪些字段。
     */
    private void replaceEntityProperty(Object parameter, String loginId, SqlCommandType sqlCommandType) {
        // 如果参数是Map类型，需要遍历Map中的每个值，对每个实体对象进行处理。
        if (parameter instanceof Map) {
            replaceMap((Map) parameter, loginId, sqlCommandType);
        } else {
            // 如果参数不是Map类型，直接对单个实体对象进行处理。
            replace(parameter, loginId, sqlCommandType);
        }
    }

    /**
     * 遍历Map类型的参数，对Map中的每个值（实体对象）进行处理。
     * 主要用于批量操作时，MyBatis传递参数的方式是通过Map。
     *
     * @param parameter 参数Map，包含了需要处理的实体对象。
     * @param loginId 当前操作用户的ID。
     * @param sqlCommandType SQL操作的类型（INSERT或UPDATE）。
     */
    private void replaceMap(Map parameter, String loginId, SqlCommandType sqlCommandType) {
        // 遍历Map中的所有值，每个值都是一个需要处理的实体对象。
        for (Object val : parameter.values()) {
            // 对每个实体对象调用replace方法进行处理。
            replace(val, loginId, sqlCommandType);
        }
    }

    /**
     * 对单个实体对象进行处理，根据SQL操作的类型（INSERT或UPDATE），填充相应的字段。
     * 对于INSERT操作，填充创建者和创建时间字段；
     * 对于UPDATE操作，填充更新者和更新时间字段。
     *
     * @param parameter 实体对象。
     * @param loginId 当前操作用户的ID。
     * @param sqlCommandType SQL操作的类型（INSERT或UPDATE）。
     */
    private void replace(Object parameter, String loginId, SqlCommandType sqlCommandType) {
        // 如果是插入操作，处理插入时需要填充的字段。
        if (SqlCommandType.INSERT == sqlCommandType) {
            dealInsert(parameter, loginId);
        } else {
            // 如果是更新操作，处理更新时需要填充的字段。
            dealUpdate(parameter, loginId);
        }
    }


    /**
     * 在执行数据库更新操作时自动填充更新者和更新时间字段。
     * 通过反射机制遍历实体对象的所有字段，如果发现特定字段为空，则自动填充。
     *
     * @param parameter 待更新的实体对象。
     * @param loginId 当前用户的ID，用于填充更新者(updateBy)字段。
     */
    private void dealUpdate(Object parameter, String loginId) {
        // 获取实体对象的所有字段，包括父类中的字段。
        Field[] fields = getAllFields(parameter);
        for (Field field : fields) {
            try {
                field.setAccessible(true); // 设置字段为可访问，即可操作私有字段。
                if (field.get(parameter) != null) {
                    // 如果字段已经被赋值，则跳过不处理。
                    continue;
                }
                // 根据字段名，对特定字段进行填充。
                switch (field.getName()) {
                    case "updateBy":
                        // 填充更新者字段。
                        field.set(parameter, loginId);
                        break;
                    case "updateTime":
                        // 填充更新时间字段。
                        field.set(parameter, new Date());
                        break;
                }
            } catch (Exception e) {
                log.error("dealUpdate.error:{}", e.getMessage(), e);
            } finally {
                field.setAccessible(false); // 重新设置字段为不可访问。
            }
        }
    }

    /**
     * 在执行数据库插入操作时自动填充创建者、创建时间以及逻辑删除标志字段。
     * 通过反射机制遍历实体对象的所有字段，如果发现特定字段为空，则自动填充。
     *
     * @param parameter 待插入的实体对象。
     * @param loginId 当前用户的ID，用于填充创建者(createdBy)字段。
     */
    private void dealInsert(Object parameter, String loginId) {
        // 获取实体对象的所有字段，包括父类中的字段。
        Field[] fields = getAllFields(parameter);
        for (Field field : fields) {
            try {
                field.setAccessible(true); // 设置字段为可访问，即可操作私有字段。
                if (field.get(parameter) != null) {
                    // 如果字段已经被赋值，则跳过不处理。
                    continue;
                }
                // 根据字段名，对特定字段进行填充。
                switch (field.getName()) {
                    case "isDeleted":
                        // 填充逻辑删除标志字段，0表示未删除。
                        field.set(parameter, 0);
                        break;
                    case "createdBy":
                        // 填充创建者字段。
                        field.set(parameter, loginId);
                        break;
                    case "createdTime":
                        // 填充创建时间字段。
                        field.set(parameter, new Date());
                        break;
                }
            } catch (Exception e) {
                log.error("dealInsert.error:{}", e.getMessage(), e);
            } finally {
                field.setAccessible(false); // 重新设置字段为不可访问。
            }
        }
    }


    /**
     * 获取对象及其所有父类的所有字段，包括私有和受保护字段。
     * 该方法用于获取一个对象的所有字段，无论这些字段位于类的层次结构的哪一层。
     *
     * @param object 需要获取字段的对象
     * @return 包含所有字段的数组
     */
    private Field[] getAllFields(Object object) {
        Class<?> clazz = object.getClass();
        List<Field> fieldList = new ArrayList<>();
        // 循环遍历对象以及其父类，直到没有超类为止
        while (clazz != null) {
            // 添加当前类的所有字段到列表中
            fieldList.addAll(Arrays.asList(clazz.getDeclaredFields()));
            // 移动到上一层父类
            clazz = clazz.getSuperclass();
        }
        // 将列表转换为数组并返回
        return fieldList.toArray(new Field[0]);
    }

    /**
     * MyBatis插件机制中的包装方法，用于创建目标对象的代理。
     * 当MyBatis决定拦截某个对象的操作时，会通过这个方法来创建一个代理对象，
     * 代理对象会在执行目标方法时触发intercept方法。
     *
     * @param target 被拦截的原始对象
     * @return 经过代理后的对象，能够在执行时触发拦截器的intercept方法
     */
    @Override
    public Object plugin(Object target) {
        // 使用MyBatis提供的Plugin类来创建代理对象
        return Plugin.wrap(target, this);
    }

    /**
     * 设置属性，可以通过配置文件传递参数给插件。
     * 本方法在插件初始化时被MyBatis调用，用于传递插件配置的属性。
     * 当前实现中未使用此方法，但它是Interceptor接口的一部分，因此需要提供实现。
     *
     * @param properties 从配置文件中传递给插件的属性
     */
    @Override
    public void setProperties(Properties properties) {
        // 在本示例中不需要使用配置属性，因此此处不做实现
    }


}
